#include <gmp.h>
#include <gmpxx.h>
#include <papi.h>

#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include <string>
#include <getopt.h>

int PAPI_start_counters(int *events, int array_len);
int PAPI_stop_counters(long_long *values, int array_len); 

//struct used for parsing arguments and deciding which tests to run
static struct option options[] = 
{
	{"int_tests", optional_argument, 0, 'i'},
	{"rational_tests", optional_argument, 0, 'r'},
	{"fp_tests", optional_argument, 0, 'f'},
	{"all_tests", optional_argument, 0, 'a'},
	{0, 0, 0, 0}
};

#define BASE 10
#define NUM_EVENTS 4
#define ONE_MILLION 1000000
#define NUM_TRIALS 20

//used for papi events
int events[NUM_EVENTS] = {PAPI_TOT_INS, PAPI_TOT_CYC, PAPI_L1_DCM, PAPI_L2_DCM};
long_long values[NUM_EVENTS];

long_long min_values[NUM_EVENTS];
double min_time;

//needed for gmp random number generation
gmp_randstate_t rand_state;

using namespace std;

void papi_start() {
	if ( PAPI_start_counters(events, NUM_EVENTS) != PAPI_OK ) {
		fprintf(stderr, "Could not start PAPI counters\n");
		exit(1);
	}
}

void papi_stop() {
	if ( PAPI_stop_counters( values, NUM_EVENTS ) != PAPI_OK ) {
		fprintf(stderr, "Could not stop PAPI counters\n");
		exit(1);
	}
}

void print_counters(long num_digits, long num_limbs) {
	cout << num_digits << " " << num_limbs << " " << min_values[0] << " " << min_values[1] << " " << min_values[2] << " " << min_values[3] << " " << min_time << endl;
}
		
double get_seconds() { /* routine to read time */
	clock_t t;
	t = clock();
	return (double) t/ (double) CLOCKS_PER_SEC;
}

void print_header(string func_name) {
	cout << "# file generated by mul.cpp - " << func_name << endl;
	cout << "# Digits Limbs Instructions Cycles L1_DCM L2_DCM Time\n";
}

void generate_random(mpz_t& num, long n) {
	mpz_t rand_max;
	mpz_init(rand_max);
	mpz_set_si(rand_max, 10);
	mpz_pow_ui(rand_max, rand_max, n);
	mpz_urandomm(num, rand_state, rand_max);
	mpz_add(num, num, rand_max);
	mpz_clear(rand_max);
}

void integer_tests() {
	//initialize gmp vars for product and addeds
	mpz_t product, multiplier, multiplicand;
	mpz_init(product);
	mpz_init(multiplier);
	mpz_init(multiplicand);
	
	//initialize the minimum values
	for ( int i = 0; i < NUM_EVENTS; i++ ) {
		min_values[i] = -1;
	}
	min_time = -1;
	
	print_header("integer_tests()");

	long min = ONE_MILLION, max = 41*ONE_MILLION, step = 2*ONE_MILLION;
	double start, stop;
	for ( long i = min; i < max; i = i+step ) {
		generate_random(multiplier, i);
		generate_random(multiplicand, i);

		for ( int j = 0; j < NUM_TRIALS; j++ ) {
			start = get_seconds();
			papi_start();
			mpz_mul(product, multiplier, multiplicand);
			papi_stop();
			stop = get_seconds();
			//check to see if the latest trial produced a new minimum value
			if ( (stop - start) < min_time || min_time < 0 ) {
				//cout << i << "\t" << min_time << "\t" << (stop-start) << endl;
				min_time = stop - start;
			}
			for ( int k = 0; k < NUM_EVENTS; k++ ) {
				if ( values[k] < min_values[k] || min_values[k] < 0 ) {
					//cout << i << "\t" << k << "\t" << min_values[k] << "\t" << values[k] << endl;
					min_values[k] = values[k];
				}
			}
		}

		print_counters(i, mpz_size(multiplier));

		min_time = -1;
		for ( int k = 0; k < NUM_EVENTS; k++ ) {
			min_values[k] = -1;
		}
	}
	
	mpz_clear(multiplier);
	mpz_clear(multiplicand);
	mpz_clear(product);
}

void rational_whole() {
	//print_header("rational_whole_tests()");
}

void rational_fractional() {
	//print_header("rational_fractional_tests()");
}

void rational_tests() {
	rational_whole();
	rational_fractional();
}

void floating_point_tests() {
	//print_header("floating_point_tests()");
}

int main(int argc, char** argv) {
	//initialize the random state using defult gmp algorithm
	gmp_randinit_default(rand_state);

	bool moreOptions = true;
	while(moreOptions) {
		int option_index = 0, c;
		c = getopt_long(argc, argv, "irfa", options, &option_index);

		if ( c == -1 ) {
			moreOptions = false;
			//user supplied no arguments so run all tests
			if ( argc == 1 ) {
				integer_tests();
				rational_tests();
				floating_point_tests();
			}
		}
		else {
			switch(c)
			{
				case 'i':
					integer_tests();
					break;
				case 'r':
					rational_tests();
					break;
				case 'f':
					floating_point_tests();
					break;
				case 'a':
					integer_tests();
					rational_tests();
					floating_point_tests();
					break;
				default:
					//unexpected arg so just give up and exit
					abort();
			}
		}
	}

	return 0;
}
